// Copyright (c) 2025 Shenzhen Kaihong Digital Industry Development Co., Ltd.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::path::{Path, PathBuf};

use serde::{de::Visitor, Deserialize, Serialize};
use sha2::{Digest, Sha256};

use crate::{deserialize_map_case, utils::{guess_filename_by_source, sha256_to_string}};

#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct DownloadConfig {
    #[serde(skip_serializing_if = "DownloadMethod::is_default")]
    pub method: DownloadMethod,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub rename: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sha256: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub git_options: Option<GitOptions>,
    #[serde(default, skip_serializing_if = "UnpackOption::is_default")]
    pub unpack: UnpackOption,
}

#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GitOptions {
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub recursive: bool,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub checkout: Option<String>,
}

#[repr(u8)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum DownloadMethod {
    Custom,
    #[default]
    Get,
    Git,
    Copy,
}

#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub enum UnpackOption {
    #[default]
    Default,
    No,
    Path(String),
}

impl Serialize for UnpackOption {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer
    {
        match self {
            UnpackOption::Default => serializer.serialize_bool(true),
            UnpackOption::No => serializer.serialize_bool(false),
            UnpackOption::Path(path) => serializer.serialize_str(path),
        }
    }
}

impl<'de> Deserialize<'de> for UnpackOption {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>
    {
        struct UnpackOptionVisitor;

        impl<'de> Visitor<'de> for UnpackOptionVisitor {
            type Value = UnpackOption;
        
            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                formatter.write_str("unpack option")
            }

            fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
                where
                    E: serde::de::Error,
            {
                match v {
                    true => Ok(UnpackOption::Default),
                    false => Ok(UnpackOption::No),
                }
            }

            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
                where
                    E: serde::de::Error,
            {
                Ok(UnpackOption::Path(v.to_string()))
            }
        }

        deserializer.deserialize_any(UnpackOptionVisitor)
    }
}

impl<'de> Deserialize<'de> for DownloadConfig {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>
    {
        #[derive(Deserialize)]
        #[serde(field_identifier, rename_all = "kebab-case")]
        enum Field {
            Method,
            Source,
            Rename,
            Sha256,
            GitOptions,
            Unpack,
        }
        
        struct DownloadConfigVisitor;

        impl<'de> Visitor<'de> for DownloadConfigVisitor {
            type Value = DownloadConfig;
        
            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                formatter.write_str("download config")
            }

            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
                where
                    A: serde::de::MapAccess<'de>
            {
                let mut method = None;
                let mut source = None;
                let mut rename = None;
                let mut sha256 = None;
                let mut git_options = None;
                let mut unpack = None;
                while let Some(k) = map.next_key()? {
                    match k {
                        Field::Method => deserialize_map_case!(map, method, "method"),
                        Field::Source => deserialize_map_case!(map, source, "source"),
                        Field::Rename => deserialize_map_case!(map, rename, "rename"),
                        Field::Sha256 => deserialize_map_case!(map, sha256, "sha256"),
                        Field::GitOptions => deserialize_map_case!(map, git_options, "git-options"),
                        Field::Unpack => deserialize_map_case!(map, unpack, "unpack"),
                    }
                }

                let method = method.unwrap_or_default();
                match method {
                    DownloadMethod::Get | DownloadMethod::Git => {
                        if source.is_none() {
                            return Err(serde::de::Error::missing_field("source"));
                        }
                    },
                    _ => {}
                }
                let unpack = unpack.unwrap_or_default();
                
                Ok(DownloadConfig {
                    method,
                    source,
                    rename,
                    sha256,
                    git_options,
                    unpack,
                })
            }
        }

        deserializer.deserialize_struct(
            "DownloadConfig",
            &["method", "source", "rename", "sha256", "git_options", "no_unpack"],
            DownloadConfigVisitor
        )
    }
}

impl DownloadConfig {
    pub fn file_name(&self) -> Option<&str> {
        if let Some(rename) = &self.rename {
            Some(rename.as_str())
        } else {
            if let Some(source) = &self.source {
                Some(guess_filename_by_source(source))
            } else {
                None
            }
        }
    }

    pub fn download_dir<P: AsRef<Path>>(&self, root: P) -> Option<PathBuf> {
        let mut hasher = Sha256::new();
        if !self.download_hash(&mut hasher) {
            return None;
        }
        let hash = hasher.finalize();
        Some(root.as_ref().join(sha256_to_string(&hash)))
    }

    fn download_hash(&self, hasher: &mut Sha256) -> bool {
        let Some(source) = self.source.as_ref() else {
            return false;
        };
        
        hasher.update([self.method as u8]);
        hasher.update(source);
        if let Some(git_options) = self.git_options.as_ref() {
            hasher.update([git_options.recursive as u8]);
            if let Some(checkout) = git_options.checkout.as_ref() {
                hasher.update(checkout);
            }
        }
        true
    }

    pub fn spec_hash(&self, hasher: &mut Sha256) {
        self.download_hash(hasher);
        if let Some(rename) = self.rename.as_ref() {
            hasher.update(rename);
        }
        self.unpack.spec_hash(hasher);
    }
}

impl DownloadMethod {
    fn is_default(&self) -> bool {
        *self == Self::default()
    }
}

impl UnpackOption {
    fn is_default(&self) -> bool {
        *self == Self::default()
    }

    fn spec_hash(&self, hasher: &mut Sha256) {
        match self {
            UnpackOption::Default => hasher.update([0]),
            UnpackOption::No => hasher.update([1]),
            UnpackOption::Path(path) => {
                hasher.update([2]);
                hasher.update(path.as_bytes());
            },
        }
    }
}

#[cfg(test)]
mod test {
    use crate::config::pkg_config::{DownloadConfig, DownloadMethod, GitOptions};

    #[test]
    fn test_git_options_serialize() {
        assert_eq!(
            toml::to_string_pretty(&GitOptions {
                recursive: true,
                checkout: Some("master".to_string())
            }),
            Ok("\
                recursive = true\n\
                checkout = \"master\"\n".to_string())
        );
        assert_eq!(
            toml::to_string_pretty(&GitOptions {
                recursive: false,
                checkout: None
            }),
            Ok("".to_string())
        );
    }

    #[test]
    fn test_git_options_deserialize() {
        assert_eq!(
            toml::from_str("\
                recursive = true\n\
                checkout = \"master\"\n"
            ),
            Ok(GitOptions {
                recursive: true,
                checkout: Some("master".to_string())
            })
        );
        assert_eq!(
            toml::from_str(""),
            Ok(GitOptions::default())
        );
    }

    #[test]
    fn test_download_config_serialize() {
        assert_eq!(
            toml::to_string_pretty(&DownloadConfig {
                method: DownloadMethod::Git,
                source: Some("url".to_string()),
                rename: None,
                sha256: None,
                git_options: Some(GitOptions {
                    recursive: true,
                    ..Default::default()
                }),
                ..Default::default()
            }),
            Ok("\
                method = \"git\"\n\
                source = \"url\"\n\
                \n\
                [git-options]\n\
                recursive = true\n".to_string())
        );
        assert_eq!(
            toml::to_string_pretty(&DownloadConfig {
                method: DownloadMethod::Get,
                source: None,
                rename: None,
                sha256: None,
                git_options: None,
                ..Default::default()
            }),
            Ok("".to_string())
        );
    }

    #[test]
    fn test_download_config_deserialize() {
        assert_eq!(
            toml::from_str("\
                method = \"git\"\n\
                source = \"url\"\n"
            ),
            Ok(DownloadConfig {
                method: DownloadMethod::Git,
                source: Some("url".to_string()),
                rename: None,
                sha256: None,
                git_options: None,
                ..Default::default()
            })
        );
        assert_eq!(
            toml::from_str("\
                source = \"url\"\n"
            ),
            Ok(DownloadConfig {
                method: DownloadMethod::Get,
                source: Some("url".to_string()),
                rename: None,
                sha256: None,
                git_options: None,
                ..Default::default()
            })
        );
        assert_eq!(
            &format!("{}", toml::from_str::<DownloadConfig>("").unwrap_err()),
            "TOML parse error at line 1, column 1\n  \
              |\n\
            1 | \n  \
              | ^\n\
            missing field `source`\n"
        );
    }
}